package com.svconnect.plugin.readers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.svconnect.plugin.model.wsdl.ComplexDataType;
import com.svconnect.plugin.model.wsdl.EComplexType;
import com.svconnect.plugin.model.wsdl.Endpoint;
import com.svconnect.plugin.model.wsdl.IDataType;
import com.svconnect.plugin.model.wsdl.Operation;
import com.svconnect.plugin.model.wsdl.Parameter;
import com.svconnect.plugin.model.wsdl.Service;
import com.svconnect.plugin.model.wsdl.SimpleDataType;
import com.svconnect.plugin.util.Log;

/**
 * A Parser class for web service definition language
 * @author Marek Chmiel
 */
public class WSDLParser {

	
	public class Namespaces {
		public static final String WSDL = "http://schemas.xmlsoap.org/wsdl/";
		public static final String SOAP = "http://schemas.xmlsoap.org/wsdl/soap/";
		public static final String SOAP12 = "http://schemas.xmlsoap.org/wsdl/soap12/";
		public static final String XSD = "http://www.w3.org/2001/XMLSchema";
		public static final String WSAW = "http://www.w3.org/2006/05/addressing/wsdl";
		
		private Map<String,String> qnamesToNs;
		private Map<String,String> nsToQnames;
		
		public Namespaces() {
			qnamesToNs = new HashMap<String,String>();
			nsToQnames = new HashMap<String,String>();
		}
		
		public void add(String qname, String uri) {
			qnamesToNs.put(qname, uri);
			nsToQnames.put(uri, qname);
		}

		public String getQName(String uri) {
			return nsToQnames.get(uri);
		}
		
		public String getUri(String qname) {
			return qnamesToNs.get(qname);
		}
		
		public boolean containsQName(String qname) {
			return qnamesToNs.containsKey(qname);
		}
		
		public boolean containsUri(String uri) {
			return nsToQnames.containsKey(uri);
		}
	}	
	
	private String url;

	private Document currentDocument;
	private Map<String, Document> xsdSchemas = new HashMap<String, Document>();
	private Map<String, Document> wsdlDefinitions = new HashMap<String, Document>();

	private String wsdlQName;
	private String soapQName;
	private String soap12QName;
	private String xsdQName;
	private String wsawQName;
	
	private Map<String,IDataType> resolvedTypes;
	private Map<String,Integer> resolvedNamesCount;
	
	private WSDLSerialization serialization;
	
	public WSDLParser(String url) {
		this.url = url;
		this.resolvedTypes = new HashMap<String,IDataType>();
		this.serialization = new WSDLSerialization();
		this.xsdSchemas = new HashMap<String, Document>();
		this.wsdlDefinitions = new HashMap<String, Document>();
		this.resolvedNamesCount = new HashMap<String,Integer>();
	}

	public String getUrl() {
		return url;
	}
	
	private void setCurrentDocument(Document current) {
		this.currentDocument = current;
		// set up common namespaces qnames		
		
		if (current!=null) {
			Namespaces namespaces = this.getDefinedNamespaces(currentDocument.getDocumentElement());
			String newWsdlQname = namespaces.getQName(Namespaces.WSDL);
			if (newWsdlQname!=null)
				this.wsdlQName = newWsdlQname;
	
			String newSoapQName = namespaces.getQName(Namespaces.SOAP);
			if (newSoapQName!=null)
				this.soapQName = newSoapQName;
			
			String newSoap12QName = namespaces.getQName(Namespaces.SOAP12);
			if (newSoap12QName!=null)
				this.soap12QName = newSoap12QName;
			
			String newXsdQName = namespaces.getQName(Namespaces.XSD);
			if (newXsdQName!=null)
				this.xsdQName = newXsdQName;
			
			String newWsasQName = namespaces.getQName(Namespaces.WSAW);
			if (newWsasQName!=null)
				this.wsawQName = newWsasQName;
		}
	}

	public List<Service> parse() throws IOException, SAXException,ParserConfigurationException {
		this.importWsdlDefinitions();
		this.importXsdSchemas();
		return this.getServices();
	}

	private void importWsdlDefinitions() throws IOException, SAXException, ParserConfigurationException {
		// get main wsdl document
		this.setCurrentDocument(this.getXmlDocument(url));
		String ns = this.getNodeAttribute(currentDocument.getDocumentElement(), "targetNamespace");
		wsdlDefinitions.put(ns, currentDocument);
		
		// import wsdl parts defined in main wsdl file
		NodeList wsdlImports = this.currentDocument.getElementsByTagName(String.format("%s:import", wsdlQName));
		int count = wsdlImports.getLength();
		for(int i=0; i<count; i++) {
			String location = this.getNodeAttribute(wsdlImports.item(i), "location");
			Document wsdlPart = this.getXmlDocument(location);
			String partNs = this.getNodeAttribute(wsdlPart.getDocumentElement(), "targetNamespace");
			wsdlDefinitions.put(partNs, wsdlPart);
		}
	}
	
	private Namespaces getDefinedNamespaces(Element rootElement) {
		Namespaces result = null;
		if (rootElement!=null) {
			NamedNodeMap attributes = rootElement.getAttributes();
			result = new Namespaces();
			for(int i=0; i<attributes.getLength(); i++) {
				Attr attr = (Attr)attributes.item(i);
				String attrName =attr.getName();
				if (attrName.equals("xmlns")) {
					result.add("",attr.getValue());
				}
				else if (attrName.startsWith("xmlns:")) {
					attrName = attrName.replace("xmlns:", "");
					result.add(attrName,attr.getValue());
				}
			}
		}
		return result;
	}
	
	private void importXsdSchemas() throws IOException, SAXException, ParserConfigurationException {
		// import xsd schemas from every wsdl file
		for(Document doc : wsdlDefinitions.values()) {
			this.importXsdSchemas(doc);
		}
	}
	
	private void importXsdSchemas(Document doc) throws IOException, SAXException, ParserConfigurationException {
		// recursively import xsd schemas
		NodeList nodes = doc.getElementsByTagName(String.format("%s:import", xsdQName));
		int count = nodes.getLength();
		for (int i = 0; i < count; i++) {
			Node node = nodes.item(i);
			String schemeLocation = this.getNodeAttribute(node,"schemaLocation");
			String ns = this.getNodeAttribute(node, "namespace");
			if (!xsdSchemas.containsKey(ns)) {
				Document xsdScheme = this.getXmlDocument(schemeLocation);
				this.xsdSchemas.put(ns,xsdScheme);
				this.importXsdSchemas(xsdScheme);
			}
		}		
	}
	private Document getXmlDocument(String url) throws IOException,
			SAXException, ParserConfigurationException {

		Log.i("Fetching " + url);
		DocumentBuilderFactory domFactory = DocumentBuilderFactory
				.newInstance();
		domFactory.setNamespaceAware(true);
		DocumentBuilder builder = domFactory.newDocumentBuilder();
		Document d = builder.parse(url);
		return d;
	}

	private List<Service> getServices() {
		
		NodeList nodes = this.currentDocument.getElementsByTagName(String.format("%s:service", wsdlQName));
		List<Service> result = new LinkedList<Service>();
		int count = nodes.getLength();
		Log.i("Found " + count + " service(s)");
		for (int i = 0; i < count; i++) {
			Element serviceNode = (Element) nodes.item(i);
			String serviceName = this.getNodeAttribute(serviceNode, "name");
			
			Element portNode = (Element)serviceNode.getElementsByTagName(String.format("%s:port", wsdlQName)).item(0);
			String bindingName = this.getNodeAttribute(portNode, "name");
			String bindingQName = this.getNodeAttribute(portNode, "binding");

			Node addressNode = null;
			boolean isSoap12 = false;
			NodeList addresses = serviceNode.getElementsByTagName(String.format("%s:address", soapQName));
			if (addresses.getLength()>0) {
				addressNode = addresses.item(0);
			}
			else {
				isSoap12 = true;
				addresses = serviceNode.getElementsByTagName(String.format("%s:address", soap12QName));
				if (addresses.getLength() > 0) {
					addressNode = addresses.item(0);
				}
				else
					throw new RuntimeException("Service is in neither SOAP 1.1 nor 1.2 format");
			}
				
			
			String location = this.getNodeAttribute(addressNode, "location");

			Endpoint endpoint = new Endpoint();
			endpoint.setAddress(location);
			endpoint.setName(bindingName);
			endpoint.setBinding((isSoap12) ? "basicHttpBinding" : "wsHttpBinding");
			
			Service svc = new Service();
			svc.setName(serviceName);
			svc.setEndpoint(endpoint);
			
			result.add(svc);
			
			Log.i("Processing " + bindingName + " binding for service "+ svc.toString());
			svc.setOperations(this.getOperations(svc, portNode, bindingQName));
		}

		return result;
	}

	private List<Operation> getOperations(Service svc, Element wsdlPortElem, String bindingQName) {
		
		this.setCurrentDocument(getWsdlDocumentFromQname(wsdlPortElem, bindingQName));
		Element bindingElem = this.getElementByAttribute(currentDocument, String.format("%s:binding", wsdlQName), "name", this.removeNamespace(bindingQName));
		
		String typeName = this.getNodeAttribute(bindingElem, "type");
		
		this.setCurrentDocument(getWsdlDocumentFromQname(bindingElem, typeName));
		Element portTypeElem = this.getElementByAttribute(currentDocument, String.format("%s:portType", wsdlQName), "name", this.removeNamespace(typeName));

		String tns = this.getNodeAttribute(currentDocument.getDocumentElement(), "targetNamespace");
		svc.setNamespace(tns);
		
		NodeList operations = portTypeElem.getElementsByTagName(String.format("%s:operation", wsdlQName));
		int count = operations.getLength();
		Log.i("Found " + count + " operation(s)");

		List<Operation> result = new LinkedList<Operation>();
		for (int i = 0; i < count; i++) {
			Element operationElem = (Element) operations.item(i);
			String operationName = this.getNodeAttribute(operationElem, "name");
			Log.i("Processing '" + operationName + "' operation");

			Element inputMessage = null;
			String inputMessageName = null;
			Element outputMessage = null;
			String outputMessageName = null;
			List<Element> faultMessages = new ArrayList<Element>();
			
			String action = null;
			NodeList children = operationElem.getChildNodes();
			for (int j = 0; j < children.getLength(); j++) {
				Element child = (Element)children.item(j);
				String nodeName = child.getNodeName();
				if (nodeName.equals(String.format("%s:input", wsdlQName))) {
					inputMessage = child;
					action = this.getNodeAttribute(child, String.format("%s:Action",wsawQName));
					inputMessageName = this.getNodeAttribute(child, "message");
				} else if (nodeName.equals(String.format("%s:output", wsdlQName))) {
					outputMessage = child;
					outputMessageName = this.getNodeAttribute(child, "message");
				} else if (nodeName.equals(String.format("%s:fault", wsdlQName))) {
					faultMessages.add(child);
				}
				
			}
			if (inputMessageName != null && outputMessageName != null && action!=null) {
				Operation op = new Operation();
				op.setName(operationName);
				op.setAction(action);
				op.setParameters(this.getOperationParameters(inputMessageName, inputMessage));
				op.setResponse(this.getOperationResponseType(outputMessageName, outputMessage));
				for(Element faultMsg : faultMessages) {
					String faultMsgName = this.getNodeAttribute(faultMsg, "message");
					op.addFault(this.getOperationFault(faultMsgName, faultMsg));
				}
				result.add(op);
			}
		}
		return result;
	}
	
	private ComplexDataType getOperationFault(String faultMsg, Element faultElem) {
		Element element = getMessageElement(faultMsg, faultElem, "detail");
		if (element != null) {
			String elementType = this.getNodeAttribute(element, "element");
			setCurrentDocument(this.getXsdDocumentFromQname(element, elementType));
			
			Element desc = this.getElementByAttribute(currentDocument, String.format("%s:element", xsdQName), "name", this.removeNamespace(elementType));
			IDataType type = this.resolveType(desc);
			((ComplexDataType)type).setType(EComplexType.EXCEPTION);
			return (ComplexDataType)type;
		}
		return null;
	}

	private List<Parameter> getOperationParameters(String messageName, Element messageElem) {
		Element element = getMessageElement(messageName,messageElem,"parameters");
		if (element != null) {
			String elementType = this.getNodeAttribute(element, "element");
			setCurrentDocument(this.getXsdDocumentFromQname(element, elementType));
			
			Element desc = this.getElementByAttribute(currentDocument, String.format("%s:element", xsdQName), "name", this.removeNamespace(elementType));

			NodeList elemList = desc.getElementsByTagName("xs:sequence").item(0).getChildNodes();
			List<Parameter> parameters = new LinkedList<Parameter>();
			for (int i = 0; i < elemList.getLength(); i++) {
				Element elem = (Element) elemList.item(i);
				String paramName = this.getNodeAttribute(elem, "name");
				Parameter param = new Parameter();
				param.setName(paramName);
				IDataType tp = this.resolveType(elem);
				param.setType(tp);
				parameters.add(param);
			}
			return parameters;
		}
		return null;
	}

	private IDataType getOperationResponseType(String messageName, Element messageElem) {
		Element element = getMessageElement(messageName,messageElem,"parameters");
		if (element != null) {
			String elementType = this.getNodeAttribute(element, "element");
			setCurrentDocument(this.getXsdDocumentFromQname(element, elementType));
			
			Element desc = this.getElementByAttribute(currentDocument, String.format("%s:element", xsdQName), "name", this.removeNamespace(elementType));

			NodeList elemList = desc.getElementsByTagName("xs:sequence")
					.item(0).getChildNodes();

			if (elemList.getLength() == 0) {
				return SimpleDataType.VOID;
			} else {
				Element elem = (Element) elemList.item(0);
				IDataType tp = this.resolveType(elem);
				return tp;
			}
		}
		return null;
	}

	private IDataType resolveType(Element xsElem) {
		String name = this.getNodeAttribute(xsElem, "name");
		String typeName = null;

		String namespace = this.getNodeAttribute(currentDocument.getDocumentElement(), "targetNamespace");
		
		Element complexTypeElem = null;
		if (xsElem.hasChildNodes()) {
			typeName = name;
			NodeList nodes = xsElem.getElementsByTagName(String.format("%s:complexType",xsdQName));
			if (nodes.getLength() > 0) {
				complexTypeElem = (Element) nodes.item(0);
			}
		} else {
			typeName = this.getNodeAttribute(xsElem, "type");
			boolean isBuiltIn = typeName.startsWith(xsdQName);
			if (!isBuiltIn) {
				
				this.setCurrentDocument(this.getXsdDocumentFromQname(xsElem, typeName));
				
				complexTypeElem = this.getElementByAttribute(currentDocument, String.format("%s:complexType", xsdQName), "name", this.removeNamespace(typeName));
				
				try {
					namespace = this.getNodeAttribute(currentDocument.getDocumentElement(), "targetNamespace");
				}
				catch(Exception ex) {}
			}
		}


		if (complexTypeElem != null) {
			if (!isAlreadyResolved(this.removeNamespace(typeName), namespace)) {
				
				ComplexDataType dt = new ComplexDataType();
				dt.setNamespace(namespace);
				dt.setName(serialization.getJavaTypeName(this.removeNamespace(typeName)));
				addResolvedType(dt);
				
				// get base class if one exists
				NodeList extNodes = complexTypeElem.getElementsByTagName(String.format("%s:extension",xsdQName));
				if (extNodes.getLength()>0) {
					Element extElem = (Element)extNodes.item(0);
					String baseClsName = this.getNodeAttribute(extElem, "base");
					if (baseClsName!=null) {
						
						this.setCurrentDocument(this.getXsdDocumentFromQname(extElem, baseClsName));
						Element el = this.getElementByAttribute(currentDocument, String.format("%s:element",xsdQName), "name", this.removeNamespace(baseClsName));

						if (el!=null) {
							IDataType baseType = this.resolveType(el);
							dt.setBaseType((ComplexDataType)baseType);
						}
					}
				}
				
				NodeList typeElements = null;
				NodeList nl = complexTypeElem.getElementsByTagName(String.format("%s:sequence",xsdQName));
				typeElements = nl.item(0).getChildNodes();

				int count = typeElements.getLength();

				Element node = (Element)typeElements.item(0);
				String maxOccurs = this.getNodeAttribute(node, "maxOccurs");
				if (maxOccurs == null)
					maxOccurs = "1";				
				
				if (count == 1 && maxOccurs.equals("unbounded")) {
						
						Parameter p = new Parameter();
						p.setName("value");
						
						//node.setAttribute("targetNamespace", namespace);
						IDataType tp = this.resolveType(node);
						p.setType(tp);
						dt.addMember(p);

						NodeList nlist = complexTypeElem.getElementsByTagName("IsDictionary");
						if (nlist.getLength() > 0) {
							dt.setType(EComplexType.DICTIONARY);
							dt.setNullable(true);
							ComplexDataType keyValPair = (ComplexDataType)dt.getMembers().get(0).getType();
							for(Parameter pa : keyValPair.getMembers()) {
								if (pa.getType() instanceof SimpleDataType) {
									SimpleDataType sdt = (SimpleDataType)pa.getType();
									sdt.setNullable(false);
									sdt.setName(serialization.getJavaTypeName(sdt.getName()));
								}
							}
						} else {
							dt.setType(EComplexType.SEQUENCE);
							dt.setNullable(true);
							IDataType innerType = dt.getMembers().get(0).getType();
							if (innerType instanceof SimpleDataType) {
								SimpleDataType sdt = (SimpleDataType)innerType;
								sdt.setNullable(false);
								sdt.setName(serialization.getJavaTypeName(sdt.getName()));
							}
						}

				} else {
					for (int i = 0; i < count; i++) {
						node = (Element) typeElements.item(i);
						Parameter p = new Parameter();
						p.setName(this.getNodeAttribute(node, "name"));
						IDataType tp = this.resolveType(node);
						p.setType(tp);
						dt.addMember(p);
					}
				}
				return dt;

			} else {
				return getResolvedType(this.removeNamespace(typeName),namespace);
			}
		} else {
			String nullableStr = xsElem.getAttribute("nillable");
			boolean nullable = (nullableStr.equals("true"));
			
			Document doc = this.getXsdDocumentFromQname(xsElem, typeName);
			Element elem = null;
			if (doc!=null) {
				this.setCurrentDocument(doc);
				elem = this.getElementByAttribute(currentDocument, String.format("%s:simpleType", xsdQName), "name", this.removeNamespace(typeName));
			}
			if (elem != null) {
				NodeList enums = elem.getElementsByTagName(String.format("%s:enumeration", xsdQName));
				if (enums.getLength() > 0) {
					ComplexDataType dt = new ComplexDataType();
					dt.setNullable(nullable);
					dt.setName(this.removeNamespace(typeName));
					dt.setType(EComplexType.ENUMERATION);
					dt.setNamespace(namespace);
					for (int i = 0; i < enums.getLength(); i++) {
						Node enumNode = enums.item(i);
						String value = this.getNodeAttribute(enumNode, "value");
						Parameter p = new Parameter();
						p.setName(value);
						dt.addMember(p);
					}
					return dt;
				}
			}

			SimpleDataType dt = new SimpleDataType();
			dt.setNullable(nullable);
			dt.setName(serialization.getJavaTypeName(this.removeNamespace(typeName),nullable));
			return dt;
		}
	}


	private Element getMessageElement(String messageName, Element elem, String messagePartName) {
		this.setCurrentDocument(this.getWsdlDocumentFromQname(elem, messageName));
		Element messageElem = this.getElementByAttribute(currentDocument, String.format("%s:message", wsdlQName), "name", this.removeNamespace(messageName));
		
		NodeList children = messageElem.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node node = children.item(i);
			if (this.getNodeAttribute(node, "name").equals(messagePartName)) {
				return (Element) node;
			}
		}
		return null;
	}


	private Element getElementByAttribute(Document doc, String tagName,
			String attrName, String attrValue) {
		NodeList bindings = doc.getElementsByTagName(tagName);
		for (int i = 0; i < bindings.getLength(); i++) {
			Element bnd = (Element) bindings.item(i);
			String attr = this.getNodeAttribute(bnd, attrName);

			if (attr != null && attr.equals(attrValue)) {
				bnd.setAttribute("targetNamespace", this.getNodeAttribute(doc.getDocumentElement(), "targetNamespace"));
				return bnd;
			}
		}
		return null;
	}

	private String getNodeAttribute(Node node, String attrName) {
		Attr nameAttr = (Attr) node.getAttributes().getNamedItem(attrName);
		return (nameAttr != null) ? nameAttr.getValue() : null;
	}
	
	private String removeNamespace(String str) {
		String[] splitted = str.split(":");
		return splitted[splitted.length-1];
	}
	
	private Document getXsdDocumentFromQname(Element elem, String objQName) {
		String uri = this.getUriFromQname(elem, objQName);
		if (uri!=null) {
			if (this.xsdSchemas!=null) {
				return this.xsdSchemas.get(uri);
			}
		}
		return null;
	}

	private Document getWsdlDocumentFromQname(Element elem, String objQName) {
		String uri = this.getUriFromQname(elem, objQName);
		if (uri!=null) {
			if (this.wsdlDefinitions!=null) {
				return this.wsdlDefinitions.get(uri);
			}
		}
		return null;
	}	
	
	private String getUriFromQname(Element elem, String objQName) {
		String[] splitted = objQName.split(":");
		String qname = splitted[0];
		Node currElem = elem;
		while (!(currElem instanceof Document)) {
			if (currElem instanceof Element) {
				Namespaces namespaces = this.getDefinedNamespaces((Element)currElem);
				if (namespaces.containsQName(qname)) {
					return namespaces.getUri(qname);
				}
			}
			currElem = currElem.getParentNode();
		}
		return null;
	}
	
	private boolean isAlreadyResolved(String typeName, String namespace) {
		IDataType type = getResolvedType(typeName, namespace);
		return (type!=null);
	}
	
	private IDataType getResolvedType(String typeName, String namespace) {
		if (this.resolvedTypes!=null) {
			String combinedName = namespace+typeName;
			
			if (this.resolvedTypes.containsKey(combinedName)) {
				IDataType type = resolvedTypes.get(combinedName);
				return type;
			}
		}
		return null;		
	}
	

	private void addResolvedType(ComplexDataType cdt) {
		if (this.resolvedTypes!=null) {
			
			int count = 0;
			if (this.resolvedNamesCount.containsKey(cdt.getName())) {
				count = this.resolvedNamesCount.get(cdt.getName());
			}
			resolvedNamesCount.put(cdt.getName(), count+1);
			
			String typeName = (count!=0) ? cdt.getName()+count : cdt.getName();
			resolvedTypes.put(cdt.getNamespace()+cdt.getName(), cdt);
			
			cdt.setName(typeName);
		}
	}	
}
